#include <cmath>
#include <shader.hpp>
#include <camera.hpp>

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#define SCREEN_WIDTH 800.0f
#define SCREEN_HEIGHT 600.0f

std::string BASE_PATH = "";

Camera cam(SCREEN_WIDTH, SCREEN_HEIGHT, glm::vec3(0.0f, 0.0f, 3.0f), glm::vec3(0.0f, 1.0f, 0.0f));

float lastX = 400, lastY = 300;
bool mouseInit = true;

unsigned int crateTexture(const char* path, int format)
{
    int width, height, nrChannels;
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    stbi_set_flip_vertically_on_load(true);
    unsigned char* data = stbi_load( path, &width, &height, &nrChannels, 0);
    if(data)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);  
    } else 
    {
        std::cerr << "Failed to load texture." << std::endl;
        glDeleteTextures(1, &texture);
        return 0;
    }
    stbi_image_free(data);
    return texture;
}

void genTriangleVAO(unsigned int &vao, unsigned int &vbo, unsigned int &ebo)
{
    float vertices[] = {
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 0.0f,

        0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        0.5f, -0.5f, -0.5f,  0.0f, 1.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
                
        0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
        0.5f, -0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
    };

    unsigned int indicies[36];
    unsigned int start = 0;
    for(int i = 0; i < 36; i += 6)
    {
        indicies[i] = start;
        indicies[i + 1] = start + 1;
        indicies[i + 2] = start + 2;
        indicies[i + 3] = start;
        indicies[i + 4] = start + 2;
        indicies[i + 5] = start + 3;
        start += 4;
    }

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    glGenBuffers(1, &ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indicies), indicies, GL_STATIC_DRAW);

    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*) 0 );
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void framebufferSizeCallback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
    return;
}

void processInput(GLFWwindow* window, float dt)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
    {
        glfwSetWindowShouldClose(window, true);
    }
    if(glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
    {
        cam.MoveForward(dt);
    }
    if(glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
    {
        cam.MoveBackward(dt);
    }
    if(glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
    {
        cam.StrideLeft(dt);
    }
    if(glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
    {
        cam.StrideRight(dt);
    }
    return;
}

void mouseCallback(GLFWwindow* window, double xpos, double ypos)
{
    if(mouseInit)
    {
        lastX = xpos;
        lastY = ypos;
        mouseInit = false;
    }
    float xOffset = xpos - lastX;
    float yOffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;

    const float sensitivity = 0.1f;
    xOffset *= sensitivity;
    yOffset *= sensitivity;

    cam.DeltaPitch(yOffset);
    cam.DeltaYaw(xOffset);
}

void scrollCallback(GLFWwindow* window, double xoffset, double yoffset)
{
    cam.DeltaFOV(yoffset);
}

int main(int argc, char* argcs[] )
{
    BASE_PATH = argcs[0];
    std::size_t found = BASE_PATH.find_last_of('\\');
    BASE_PATH = BASE_PATH.substr(0, found);

    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(SCREEN_WIDTH, SCREEN_HEIGHT, "LearnOpenGL", NULL, NULL);
    if(window == NULL)
    {
        std::cout << "Failed to create GLFW window." << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    
    if(!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD." << std::endl;
        return -1;
    }

    glViewport(0, 0, 800, 600);
    glfwSetFramebufferSizeCallback(window, framebufferSizeCallback);
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
    glfwSetCursorPosCallback(window, mouseCallback);
    glfwSetScrollCallback(window, scrollCallback);

    glEnable(GL_DEPTH_TEST);

    std::string vsPath = BASE_PATH + "\\lesson7_vs.glsl";
    std::string fsPath = BASE_PATH + "\\lesson7_fs.glsl";
    Shader shaderObj(vsPath.c_str(), fsPath.c_str());
    if(!shaderObj.ready()){ return -1; }

    std::string lvsPath = BASE_PATH + "\\lightsource_vs.glsl";
    std::string lfsPath = BASE_PATH + "\\lightsource_fs.glsl";
    Shader lightObj(lvsPath.c_str(), lfsPath.c_str());
    if(!lightObj.ready()){ return -1; }

    glm::vec3 objectColor(1.0f, 0.5f, 0.31f);
    glm::vec3 lightColor(1.0f, 1.0f, 1.0f);

    unsigned int vao;
    unsigned int vbo;
    unsigned int ebo;
    genTriangleVAO(vao, vbo, ebo);
    
    glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
    glm::vec3 cubePos( 0.0f,  0.0f,  0.0f);
    
    float curTs = glfwGetTime();
    float lastTs;
    while(!glfwWindowShouldClose(window)){
        lastTs = curTs;
        curTs = glfwGetTime();
        processInput(window, curTs - lastTs);
        
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glm::mat4 view = cam.GetViewMatrix();
        glm::mat4 projection = cam.GetPerspective(); 

        glm::mat4 model;
        
        model = glm::translate( glm::mat4(1.0f), cubePos);

        shaderObj.use();
        glBindVertexArray(vao);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        shaderObj.setMat4("viewMat", view);
        shaderObj.setMat4("projMat", projection);
        shaderObj.setMat4("modelMat", model);
        shaderObj.setVec3("objectColor", objectColor);
        shaderObj.setVec3("lightColor", lightColor);
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

        model = glm::translate(glm::mat4(1.0), lightPos);
        model = glm::scale(model, glm::vec3(0.2f));
    
        lightObj.use();
        glBindVertexArray(vao);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        lightObj.setMat4("viewMat", view);
        lightObj.setMat4("projMat", projection);
        lightObj.setMat4("modelMat", model);
        glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);       
        glBindVertexArray(0);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glDeleteBuffers(1, &vao);
    glDeleteBuffers(1, &vbo);
    glDeleteBuffers(1, &ebo);

    glfwTerminate();   
    return 0;
}